package jp.co.nes.awssupport.common.utility;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
import com.amazonaws.services.s3.model.S3Object;

import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.AppInfoBean;
import jp.co.nes.awssupport.common.bean.AppInfoContainerBean;

/**
 * アプリケーション情報ユーティリティです。<br>
 * 以下の方法でアプリケーション情報を読み込みます。
 * <p>
 * 【読み込み方法】<br>
 * {@link PropertiesUtil}を使用し、AppInfoUseServiceキーの内容を読み込み、その内容に従い、アプリケーション情報を取得します。<br>
 * AppInfoUseServiceキーに設定可能な値は「DynamoDB」、「S3」、「ClassPath」になります。<br>
 * AppInfoUseServiceキーが設定されていない場合は「S3」になります。
 * </p>
 * <p>
 * 「DynamoDB」の場合は、AppInfoテーブルの内容を使用します。<br>
 * なお、AppInfoテーブルは事前に作成され、アプリケーション情報が格納されていることが前提になっています。
 * </p>
 * <p>
 * 「S3」の場合は、s3に配置されているYamlファイルの内容を使用します。<br>
 * なお、GZIP圧縮したYamlファイルを使用することも可能です。<br>
 * 上記、Yamlファイルの配置場所は、{@link PropertiesUtil}を使用し、読み込んだ、以下の内容になります。<br>
 * ・AppInfoBucketNameキー：s3のバケット名(設定されていない場合はawssupport-appinfo)。<br>
 * ・AppInfoObjectKeyキー：s3のオブジェクト名(設定されていない場合はAppInfo.yaml.gz)。
 * </p>
 * <p>
 * 「ClassPath」の場合は、クラスパスのルートに配置されているYamlファイルの内容を使用します。<br>
 * なお、GZIP圧縮したYamlファイルを使用することも可能です。<br>
 * 上記、Yamlファイル名は、{@link PropertiesUtil}を使用し、読み込んだ、以下の内容になります。<br>
 * ・AppInfoFileName：Yamlファイル名(GZIPで圧縮している場合は拡張子をgzにする必要があります)(設定されていない場合はAppInfo.yaml.gz)
 * </p>
 * <p>
 * 【その他】
 * Lambda関数の場合、上述した内容に加えて、{@link PropertiesUtil}を使用し、AppInfoPrefixOfLambdaEnvキーの内容を読み込み、
 * AppInfoPrefixOfLambdaEnvキーに設定された値がプリフィックスになっている情報を、Lambda関数に設定されている環境情報の
 * 内容の中から読み込み、アプリケーション情報に設定します。<br>
 * なお、同じ内容が存在した場合は、Lambda関数に設定されている環境情報の内容が優先されます。
 * </p>
 */
public class AppInfoUtil {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(AppInfoUtil.class);

    /** アプリケーション情報を格納するために使用する内容をあらわす列挙型 */
    private static enum AppInfoUseService {
        DYNAMO_DB, S3, CLASS_PATH
    }

    /** アプリケーション情報マップ */
    private static Map<String, AppInfoBean> appInfoMap = new HashMap<>();

    /** 再読み込み可能フラグ(true: 可能, false: 可能でない) */
    private static boolean isReloadPossible = true;

    /**
     * 初期化処理です。
     */
    static {
        String lambdaFunctionName = System.getenv("AWS_LAMBDA_FUNCTION_NAME");
        if (lambdaFunctionName == null) {
            // Lambda関数以外の場合
            // Lambda関数の場合、リクエストごとに、reload()メソッドを呼び出し、再ロードする方針にしているので、
            // ここで環境情報を読み込むと、2重読み込みになるため、Lambda関数以外の場合に読み込む方針にしている。
            logger.info("AppInfo load start");
            load();
            logger.info("AppInfo load end");
        }
    }

    /**
     * デフォルトコンストラクタです。<br>
     * 本クラスはインスタンスを生成しません。
     */
    private AppInfoUtil() {
    }

    /**
     * 再読み込み可能フラグを設定します。
     *
     * @param isPossible 再読み込み可能フラグ(true: 可能, false: 可能でない)
     */
    public static void setReloadPossible(boolean isPossible) {
        isReloadPossible = isPossible;
    }

    /**
     * アプリケーション情報を再読み込みします。
     */
    public static void reload() {
        if (isReloadPossible) {
            logger.info("AppInfo reload start");
            load();
            logger.info("AppInfo reload end");
        }
    }

    //========================================================================================================
    // 以下、アプリケーション情報からカテゴリ、番号(or 省略され0固定)、キーに対応する値を返却するメソッド。
    //========================================================================================================

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値
     */
    public static String getVal(String category, String key) {
        return getVal(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値
     */
    public static String getVal(String category, int number, String key) {
        String result = null;
        AppInfoBean appInfoBean = appInfoMap.get(category + number);
        if (appInfoBean != null) {
            Map<String, String> info = appInfoBean.getInfo();
            if (info != null) {
                result = info.get(key);
            }
        }
        return result;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntVal(String category, String key) {
        return getIntVal(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntVal(String category, int number, String key) {
        return Integer.parseInt(getVal(category, number, key));
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongVal(String category, String key) {
        return getLongVal(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongVal(String category, int number, String key) {
        return Long.parseLong(getVal(category, number, key));
    }

    //========================================================================================================
    // 以下、アプリケーション情報からカテゴリ、番号(or 省略され0固定)、キーに対応する値を返却するメソッド。
    // また、返却前に値がnull or 空文字列かチェックする。
    // null or 空文字列だった場合は、CommonException例外が発生。
    //========================================================================================================

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値
     */
    public static String getValAndEmptyChk(String category, String key) {
        return getValAndEmptyChk(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値
     */
    public static String getValAndEmptyChk(String category, int number, String key) {
        String value = getVal(category, number, key);
        if (StringUtils.isEmpty(value)) {
            throw new CommonException(category + "," + number + "," + key + " is null or empty");
        }
        return value;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndEmptyChk(String category, String key) {
        return getIntValAndEmptyChk(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndEmptyChk(String category, int number, String key) {
        return Integer.parseInt(getValAndEmptyChk(category, number, key));
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndEmptyChk(String category, String key) {
        return getLongValAndEmptyChk(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェックします。<br>
     * null or 空文字列だった場合は、{@link CommonException}例外がスローされます。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndEmptyChk(String category, int number, String key) {
        return Long.parseLong(getValAndEmptyChk(category, number, key));
    }

    //========================================================================================================
    // 以下、アプリケーション情報からカテゴリ、番号(or 省略され0固定)、キーに対応する値を返却するメソッド。
    // また、返却前に値がnull or 空文字列かをチェック and 値が最小値より小さいかをチェック。
    // null or 空文字列だった場合、最小値より小さい場合は、CommonException例外が発生。
    //========================================================================================================

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェック and 値が最小値より小さいかをチェックします。<br>
     * null or 空文字列だった場合 or 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param minValue 最小値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndEmptyMinChk(String category, String key, int minValue) {
        return getIntValAndEmptyMinChk(category, 0, key, minValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェック and 値が最小値より小さいかをチェックします。<br>
     * null or 空文字列だった場合 or 最小値より小さい場合は、{@link CommonException}例外がスローされます。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param minValue 最小値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndEmptyMinChk(String category, int number, String key, int minValue) {
        int value = getIntValAndEmptyChk(category, number, key);
        if (value < minValue) {
            throw new CommonException(
                    "min error(" + category + "," + number + "," + key + "," + minValue + "=" + value + ")");
        }
        return value;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェック and 値が最小値より小さいかをチェックします。<br>
     * null or 空文字列だった場合 or 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param minValue 最小値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndEmptyMinChk(String category, String key, long minValue) {
        return getLongValAndEmptyMinChk(category, 0, key, minValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * なお、返却前に値がnull or 空文字列かチェック and 値が最小値より小さいかをチェックします。<br>
     * null or 空文字列だった場合 or 最小値より小さい場合は、{@link CommonException}例外がスローされます。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param minValue 最小値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndEmptyMinChk(String category, int number, String key, long minValue) {
        long value = getLongValAndEmptyChk(category, number, key);
        if (value < minValue) {
            throw new CommonException(
                    "min error(" + category + "," + number + "," + key + "," + minValue + "=" + value + ")");
        }
        return value;
    }

    //========================================================================================================
    // 以下、アプリケーション情報からカテゴリ、番号(or 省略され0固定)、キーに対応する値(デフォルト値有)を返却するメソッド。
    //========================================================================================================

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値
     */
    public static String getVal(String category, String key, String defaultValue) {
        return getVal(category, 0, key, defaultValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値
     */
    public static String getVal(String category, int number, String key, String defaultValue) {
        String result = getVal(category, number, key);
        if (StringUtils.isEmpty(result)) {
            result = defaultValue;
        }
        return result;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntVal(String category, String key, int defaultValue) {
        return getIntVal(category, 0, key, defaultValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntVal(String category, int number, String key, int defaultValue) {
        int result = defaultValue;
        String value = getVal(category, number, key);
        if (!StringUtils.isEmpty(value)) {
            result = Integer.parseInt(value);
        }
        return result;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongVal(String category, String key, long defaultValue) {
        return getLongVal(category, 0, key, defaultValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param defaultValue デフォルト値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongVal(String category, int number, String key, long defaultValue) {
        long result = defaultValue;
        String value = getVal(category, number, key);
        if (!StringUtils.isEmpty(value)) {
            result = Long.parseLong(value);
        }
        return result;
    }

    //========================================================================================================
    // 以下、アプリケーション情報からカテゴリ、番号(or 省略され0固定)、キーに対応する値(デフォルト値有)を返却するメソッド。
    // また、返却前に値が最小値より小さいかをチェック。
    // 値が引数の最小値より小さい場合は、CommonException例外が発生。
    //========================================================================================================

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、返却前に値が最小値より小さいかをチェックします。<br>
     * 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param defaultValue デフォルト値
     * @param minValue 最小値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndMinChk(String category, String key, int defaultValue, int minValue) {
        return getIntValAndMinChk(category, 0, key, defaultValue, minValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、返却前に値が最小値より小さいかをチェックします。<br>
     * 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param defaultValue デフォルト値
     * @param minValue 最小値
     * @return 値(int型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static int getIntValAndMinChk(String category, int number, String key, int defaultValue, int minValue) {
        int value = getIntVal(category, number, key, defaultValue);
        if (value < minValue) {
            throw new CommonException("min error(" + category + "," + number + "," + key + "," + defaultValue + ","
                    + minValue + "=" + value + ")");
        }
        return value;
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、返却前に値が最小値より小さいかをチェックします。<br>
     * 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param defaultValue デフォルト値
     * @param minValue 最小値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndMinChk(String category, String key, long defaultValue, long minValue) {
        return getLongValAndMinChk(category, 0, key, defaultValue, minValue);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を返却します。<br>
     * 値が存在しなかった場合はデフォルトを返却します。<br>
     * なお、返却前に値が最小値より小さいかをチェックします。<br>
     * 最小値より小さい場合は、{@link CommonException}例外がスローされます。<br>
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param defaultValue デフォルト値
     * @param minValue 最小値
     * @return 値(long型に変換、変換に失敗した場合はNumberFormatException例外をスロー)
     */
    public static long getLongValAndMinChk(String category, int number, String key, long defaultValue, long minValue) {
        long value = getLongVal(category, number, key, defaultValue);
        if (value < minValue) {
            throw new CommonException("min error(" + category + "," + number + "," + key + "," + defaultValue + ","
                    + minValue + "=" + value + ")");
        }
        return value;
    }

    //========================================================================================================
    // 以下、カテゴリ、番号(or 省略され0固定)に対応するアプリケーション情報のキーと値のマップを返却するメソッド。
    //========================================================================================================

    /**
     * カテゴリに対応するアプリケーション情報のキーと値のマップを返却します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @return アプリケーション情報のキーと値のマップ
     */
    public static Map<String, String> getMap(String category) {
        return getMap(category, 0);
    }

    /**
     * カテゴリ、番号に対応するアプリケーション情報のキーと値のマップを返却します。<br>
     *
     * @param category カテゴリ
     * @param number 番号
     * @return アプリケーション情報のキーと値のマップ
     */
    public static Map<String, String> getMap(String category, int number) {
        Map<String, String> result = new HashMap<>();
        AppInfoBean appInfoBean = appInfoMap.get(category + number);
        if (appInfoBean != null) {
            Map<String, String> info = appInfoBean.getInfo();
            if (info != null) {
                result.putAll(info);
            }
        }
        return result;
    }

    //========================================================================================================
    // 以下、カテゴリに対応するアプリケーション情報のリストを返却するメソッド。
    //========================================================================================================

    /**
     * カテゴリに対応するアプリケーション情報のリストを返却します。<br>
     * カテゴリに対応するアプリケーション情報のリストは、番号が0のものから順番に取得し、リストに登録します。<br>
     * 番号が終わると、リストに登録するのを終了します。<br>
     * 番号が途中で抜けていると、その抜けている番号以降のものがリストに登録されないので注意してください。
     *
     * @param category カテゴリ
     * @return アプリケーション情報のリスト
     */
    public static List<AppInfoBean> getList(String category) {
        List<AppInfoBean> result = new ArrayList<>();
        int number = 0;
        while (true) {
            AppInfoBean appInfoBean = appInfoMap.get(category + number);
            if (appInfoBean == null) {
                break;
            }
            result.add(appInfoBean);
            number++;
        }
        return result;
    }

    /**
     * カテゴリに対応するアプリケーション情報のキーと値のマップが格納されたリストを返却します。<br>
     * 番号が終わると、リストに登録するのを終了します。<br>
     * 番号が途中で抜けていると、その抜けている番号以降のものがリストに登録されないので注意してください。
     *
     * @param category カテゴリ
     * @return アプリケーション情報のキーと値のマップが格納されたリスト
     */
    public static List<Map<String, String>> getListIncludingMap(String category) {
        List<Map<String, String>> result = new ArrayList<>();
        int number = 0;
        while (true) {
            AppInfoBean appInfoBean = appInfoMap.get(category + number);
            if (appInfoBean == null) {
                break;
            }

            Map<String, String> addMap = new HashMap<>();
            Map<String, String> info = appInfoBean.getInfo();
            if (info != null) {
                addMap.putAll(info);
            }
            result.add(addMap);
            number++;
        }
        return result;
    }

    //========================================================================================================
    // 以下は、アプリケーション情報の設定、読み込み、保存等に関するメソッド
    //========================================================================================================

    /**
     * アプリケーション情報に対してカテゴリ、キーに対応する値を設定します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     * @param value 値
     */
    public static void setVal(String category, String key, String value) {
        setVal(category, 0, key, value);
    }

    /**
     * アプリケーション情報に対してカテゴリ、番号、キーに対応する値を設定します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     * @param value 値
     */
    public static void setVal(String category, int number, String key, String value) {
        AppInfoBean appInfoBean = appInfoMap.get(category + number);
        if (appInfoBean == null) {
            appInfoBean = new AppInfoBean();
            appInfoMap.put(category + number, appInfoBean);
        }
        Map<String, String> info = appInfoBean.getInfo();
        if (info == null) {
            info = new HashMap<>();
            appInfoBean.setInfo(info);
        }
        info.put(key, value);
    }

    /**
     * アプリケーション情報からカテゴリ、キーに対応する値を削除します。<br>
     * なお、番号は0になります。
     *
     * @param category カテゴリ
     * @param key キー
     */
    public static void removeVal(String category, String key) {
        removeVal(category, 0, key);
    }

    /**
     * アプリケーション情報からカテゴリ、番号、キーに対応する値を削除します。
     *
     * @param category カテゴリ
     * @param number 番号
     * @param key キー
     */
    public static void removeVal(String category, int number, String key) {
        AppInfoBean appInfoBean = appInfoMap.get(category + number);
        if (appInfoBean == null) {
            return;
        }
        Map<String, String> info = appInfoBean.getInfo();
        if (info == null) {
            return;
        }
        info.remove(key);
    }

    /**
     * 引き渡された入力ストリームの内容から、アプリケーション情報を読み込み、DynamnoDBに保存します。<br>
     * 本メソッドは、複数プロセス・スレッド等から、同時に呼び出されない前提の実装になっていますので、
     * 呼び出す際は、同時に呼び出さない実装にして下さい。
     *
     * @param inputStream 入力ストリーム
     * @param rc ReadCapacityUnits(アプリケーション情報)
     * @param wc WriteCapacityUnits(アプリケーション情報)
     */
    public static void saveDynamoDb(InputStream inputStream, long rc, long wc) {
        createTableAppInfo(rc, wc);
        if (inputStream != null) {
            AppInfoContainerBean container = YamlUtil.convertYamlInputStreamToBean(inputStream,
                    AppInfoContainerBean.class);
            List<AppInfoBean> appInfoList = container.getAppInfoList();
            if (appInfoList != null) {
                // なお、本メソッドが使用されるタイミング(通常運用時)から削除するオブジェクトのリスト取得について、
                // ConsistentReadで呼び出す必要なしと判断している。
                List<AppInfoBean> deleteList = getAppInfoListFromDynamoDb();
                deleteList.stream().parallel().forEach(bean -> {
                    // parallelでアプリケーション情報を削除する。
                    AwsDynamoDBHelper helper = new AwsDynamoDBHelper();
                    helper.delete(bean);
                });
                appInfoList.stream().parallel().forEach(bean -> {
                    // parallelでアプリケーション情報を保存する。
                    AwsDynamoDBHelper helper = new AwsDynamoDBHelper();
                    helper.save(bean);
                });
            }
        }
    }

    /**
     * アプリケーション情報を読み込みます。
     */
    private static void load() {
        synchronized (appInfoMap) {
            appInfoMap.clear();

            String useServiceValue = PropertiesUtil.getProperty("AppInfoUseService", AppInfoUseService.S3.name());
            if (logger.isDebugEnabled()) {
                logger.debug("AppInfoUseService=" + useServiceValue);
            }
            AppInfoUseService useService = AppInfoUseService.valueOf(useServiceValue);

            if (useService == AppInfoUseService.DYNAMO_DB) {
                logger.debug("DynamoDB: getAppInfoListFromDynamoDb start");
                List<AppInfoBean> appInfoList = getAppInfoListFromDynamoDb();
                logger.debug("DynamoDB: getAppInfoListFromDynamoDb end");

                for (AppInfoBean appInfoBean : appInfoList) {
                    appInfoMap.put(appInfoBean.getCategory() + appInfoBean.getNumber(), appInfoBean);
                }
            } else if (useService == AppInfoUseService.S3 || useService == AppInfoUseService.CLASS_PATH) {
                InputStream inputStream = null;
                if (useService == AppInfoUseService.S3) {
                    String bucketName = PropertiesUtil.getProperty("AppInfoBucketName", "awssupport-appinfo");
                    String objectKey = PropertiesUtil.getProperty("AppInfoObjectKey", "AppInfo.yaml.gz");

                    logger.debug("S3: getAppInfoInputStreamFromS3 start");
                    if (objectKey.endsWith(".gz")) {
                        InputStream in = getAppInfoInputStreamFromS3(bucketName, objectKey);
                        try {
                            inputStream = new GZIPInputStream(in);
                        } catch (IOException e) {
                            throw new CommonException(e);
                        }
                    } else {
                        inputStream = getAppInfoInputStreamFromS3(bucketName, objectKey);
                    }
                    logger.debug("S3: getAppInfoInputStreamFromS3 end");
                } else if (useService == AppInfoUseService.CLASS_PATH) {
                    String fileName = PropertiesUtil.getProperty("AppInfoFileName", "AppInfo.yaml.gz");
                    if (fileName.endsWith(".gz")) {
                        InputStream in = ClassLoader.getSystemResourceAsStream(fileName);
                        try {
                            inputStream = new GZIPInputStream(in);
                        } catch (IOException e) {
                            throw new CommonException(e);
                        }
                    } else {
                        inputStream = ClassLoader.getSystemResourceAsStream(fileName);
                    }
                }

                if (inputStream != null) {
                    try {
                        AppInfoContainerBean container = YamlUtil.convertYamlInputStreamToBean(inputStream,
                                AppInfoContainerBean.class);
                        List<AppInfoBean> appInfoList = container.getAppInfoList();
                        for (AppInfoBean infoBean : appInfoList) {
                            String key = infoBean.getCategory() + infoBean.getNumber();
                            appInfoMap.put(key, infoBean);
                        }
                    } finally {
                        try {
                            inputStream.close();
                        } catch (IOException e) {
                            throw new CommonException(e);
                        }
                    }
                }
            } else {
                // 通常発生しない。
                throw new CommonException("useService is wrong");
            }

            String lambdaFunctionName = System.getenv("AWS_LAMBDA_FUNCTION_NAME");
            if (lambdaFunctionName != null) {
                // Lambda関数の場合、AppInfoPrefixOfLambdaEnvキーに設定された値がプリフィックスになっている情報を、
                // Lambda関数に設定されている環境情報の内容の中から読み込み、アプリケーション情報に設定する。
                logger.debug("Lambda: appInfo start");

                String prefixValue = PropertiesUtil.getProperty("AppInfoPrefixOfLambdaEnv");
                if (!StringUtils.isEmpty(prefixValue)) {
                    String[] prefixArray = prefixValue.split(",");
                    Map<String, String> lambdaEnvMap = System.getenv();
                    for (String prefix : prefixArray) {
                        List<String> keyList = lambdaEnvMap.keySet().stream().filter(key -> {
                            return !StringUtils.isEmpty(key) && key.startsWith(prefix);
                        }).collect(Collectors.toList());

                        for (String key : keyList) {
                            String category = prefix.substring(0, prefix.indexOf("_"));
                            String numberValue = prefix.substring(prefix.indexOf("_") + 1, prefix.length() - 1);
                            String value = lambdaEnvMap.get(key);

                            AppInfoBean infoBean = appInfoMap.get(category + numberValue);
                            if (infoBean == null) {
                                infoBean = new AppInfoBean();
                                infoBean.setCategory(category);
                                infoBean.setNumber(Integer.parseInt(numberValue));
                                appInfoMap.put(category + numberValue, infoBean);
                            }
                            Map<String, String> info = infoBean.getInfo();
                            if (info == null) {
                                info = new HashMap<>();
                                infoBean.setInfo(info);
                            }

                            String infoKey = key.substring(key.lastIndexOf("_") + 1);
                            info.put(infoKey, value);
                        }
                    }
                }

                logger.debug("Lambda: appInfo start");
            }
        }
    }

    /**
     * s3からアプリケーション情報を読み込み、入力ストリームを返却します。
     *
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @return 入力ストリーム
     */
    private static InputStream getAppInfoInputStreamFromS3(String bucketName, String objectKey) {
        AwsS3Helper helper = new AwsS3Helper();
        S3Object s3Object = helper.getObject(bucketName, objectKey);
        return s3Object.getObjectContent();
    }

    /**
     * DynamoDBにアプリケーション情報のテーブルを作成します。
     *
     * @param rc ReadCapacityUnits(アプリケーション情報)
     * @param wc WriteCapacityUnits(アプリケーション情報)
     */
    private static void createTableAppInfo(long rc, long wc) {
        AwsDynamoDBHelper helper = new AwsDynamoDBHelper();
        helper.createTable(AppInfoBean.class, rc, wc);
    }

    /**
     * DynamoDBから読み込んだアプリケーション情報リストを返却します。
     *
     * @return DynamoDBから読み込んだアプリケーション情報リスト
     */
    private static List<AppInfoBean> getAppInfoListFromDynamoDb() {
        AwsDynamoDBHelper helper = new AwsDynamoDBHelper();
        DynamoDBScanExpression scanExpression = new DynamoDBScanExpression();
        List<AppInfoBean> scanList = helper.scan(AppInfoBean.class, scanExpression);
        if (scanList == null) {
            scanList = new ArrayList<>();
        }
        return scanList;
    }
}
